#!/usr/bin/env bash

###############################################################
# File Name: install_redis_cluster_six_nodes.sh
# Version: V1.0
# Author: Brian Hsiung
# Created Time : 2025-01-22 9:37:53
# Description: Redis Cluster 部署脚本
##############################################################

# Redis 运行用户
declare -r REDIS_USER="redis"
# Redis版本
declare -r REDIS_VERSION="6.2.20"
# Redis基础目录
declare -r REDIS_BASE="/data/redis"
# Redis 日志和进程文件目录
declare -r REDIS_LOG_DIR="$REDIS_BASE/log"
# Redis 数据目录
declare -r REDIS_DATA_DIR="$REDIS_BASE/data"
# Redis 配置文件目录
declare -r REDIS_CONF_DIR="$REDIS_BASE/conf"
# Redis 客户端端口
declare -r REDIS_CLIENT_PORT="6379"
# Redis 集群通信端口 REDIS_CLIENT_PORT + 10000
declare -ri REDIS_CLUSTER_PORT=$((REDIS_CLIENT_PORT + 10000))
# Redis Client 连接数
declare -r REDIS_MAXCLIENTS="20000"
# Redis 最大可用内存（4g = 4*1024*1204*1024）
declare -r REDIS_MAX_MEMORY="4294967296"
# 是否开启appendonly (默认为no)
declare -r APPENDONLY="no"
# Redis 密码
declare -r REDIS_PASSWORD="KPMMo!buH8.!pe6vbvX3Mvi"
# 集群IP数组，用于关联集群（IP根据实际情况修改）
declare -a REDIS_CLUSTER_IP=("172.26.85.160" "172.26.85.161" "172.26.85.162" "172.26.85.163" "172.26.85.164" "172.26.85.165")

# 当前主机IP
REDIS_IP="$(hostname -I|awk '{print $1}')"

WORK_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$WORK_DIR" || exit

### 日志函数
log_info()    { echo -e "\033[34m[INFO] $*\033[0m"; }
log_warn()    { echo -e "\033[33m[WARNING] $*\033[0m"; }
log_success() { echo -e "\033[32m[SUCCESS] $*\033[0m"; }
log_error()   { echo -e "\033[31m[ERROR] $*\033[0m"; exit 1; }

[[ "$EUID" -ne 0 ]] && log_error "请使用root用户执行此脚本"

### 检查依赖
check_dependency() {
    local -a required_cmds=("gcc" "make" "wget" "tar" "groupadd" "useradd")
    local missing=()

    for cmd in "${required_cmds[@]}"; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done

    ((${#missing[@]})) && log_error "缺失依赖命令: ${missing[*]}"
}

### 检查端口
check_ports() {
    local -a ports=("$REDIS_CLIENT_PORT" "$REDIS_CLUSTER_PORT")
    local used_ports=()

    for port in "${ports[@]}"; do
        if ss -lntu | grep -q ":$port " &>/dev/null; then
            used_ports+=("$port")
        fi
    done

    ((${#used_ports[@]})) && log_error "端口 ${used_ports[*]} 被占用"

    if systemctl is-active firewalld &>/dev/null; then
        log_info "防火墙已开启，开放端口 $REDIS_CLIENT_PORT $REDIS_CLUSTER_PORT"
        for port in "${ports[@]}"; do
            firewall-cmd --zone=public --add-port="$port"/tcp --permanent
        done
        firewall-cmd --reload
    fi
}

### 创建目录
create_directory() {
    local dir="$1"
    if [ ! -d "$dir" ]; then
        log_info "创建目录 $dir"
        mkdir -p "$dir"
    elif [[ "$(ls -A "$dir")" ]]; then
        log_error "目录 $dir 已经存在且不为空" 
    fi
}

### 设置目录权限（递归检查上级目录）
set_parent_permissions() {
    # 某些等保三服务器默认新建的的目录权限为700，文件权限为600，导致服务无法启动，遍历上级目录，将权限修改为755
    # 获取真实目录
    local target_dir
    target_dir=$(realpath "$1")
    # 获取上级目录
    local current_dir
    current_dir=$(dirname "$target_dir")

    # 做个判断，避免进入死循环
    [ -d "$target_dir" ] || log_error "目录不存在 [$target_dir]"

    # 排除敏感目录
    local -a exclude_dirs
    exclude_dirs=("/usr" "/usr/local" "/bin" "/sbin" "/lib" "/etc" "/dev" "/var" "/root" "/usr/share")

    while [[ "$current_dir" != "/" ]]; do

        # 排除敏感目录
        if printf '%s\n' "${exclude_dirs[@]}" | grep -qx "$current_dir"; then
            log_info "跳过系统保护目录: $current_dir"
            current_dir=$(dirname "$current_dir")
            continue
        fi

        # 跳过不存在的目录
        if [[ ! -d "$current_dir" ]]; then
            log_warn "警告: 目录不存在 [$current_dir]"
            current_dir=$(dirname "$current_dir")
            continue
        fi
        
        # 获取当前目录权限
        local perm_oct
        perm_oct=$(stat -c '%a' "$current_dir")
        log_info "目录 $current_dir 的原始权限为: $perm_oct"
        
        if [[ "$perm_oct" != "755" ]]; then
            log_info "正在设置权限: [755] -> $current_dir"
            if ! chmod 755 "$current_dir"; then
                log_error "错误: 无法设置目录权限 [$current_dir]"
            fi
        else
            log_success "权限正确 [755]: $current_dir"
        fi

        # 向上一级目录
        current_dir=$(dirname "$current_dir")
    done
}

### 初始化环境
init_env() {

    if ! getent group "$REDIS_USER" >/dev/null 2>&1; then
        log_info "添加用户组 $REDIS_USER"
        groupadd -r "$REDIS_USER"
    fi

    if ! getent passwd "$REDIS_USER" >/dev/null 2>&1; then
        log_info "添加用户 $REDIS_USER"
        useradd -M -r -g "$REDIS_USER" "$REDIS_USER" -s /sbin/nologin
    fi

    log_info "调整内核参数"
    /bin/cp -af /etc/sysctl.conf "/etc/sysctl.conf.bak.$(date '+%Y%m%d%H%M%S')"
    # 内核参数设置（统一使用sed替换模式）
    sed -i -r '/^(net.core.somaxconn|vm.overcommit_memory)/d' /etc/sysctl.conf
    cat << EOF >> /etc/sysctl.conf
vm.overcommit_memory = 1
net.core.somaxconn = 4096
EOF
    sysctl -p
    # 透明大页设置
    log_info "透明大页设置"
    tee /sys/kernel/mm/transparent_hugepage/{enabled,defrag} <<< never
    # 开机启动配置
    grep -q 'transparent_hugepage' /etc/rc.d/rc.local || cat << EOF >> /etc/rc.d/rc.local
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
    chmod +x /etc/rc.d/rc.local

    # 创建目录
    [ -d "$REDIS_BASE" ] || mkdir -p "$REDIS_BASE" 
    create_directory "$REDIS_LOG_DIR"
    create_directory "$REDIS_DATA_DIR"
    create_directory "$REDIS_CONF_DIR"

    # 设置目录权限
    log_info "设置目录权限"
    set_parent_permissions "$REDIS_BASE"
    chown "$REDIS_USER":"$REDIS_USER" -R "$REDIS_BASE"
    
}

### 下载编译redis
install_make_redis() {

    local redis_pkg
    redis_pkg="redis-$REDIS_VERSION.tar.gz"

    # 服务器可访问外网时
    if curl -sIfo /dev/null --max-time 5 --connect-timeout 10 https://download.redis.io; then
        if [ ! -f "$redis_pkg" ]; then
            log_info "下载 $redis_pkg"
            # https://download.redis.io/releases/redis-6.2.20.tar.gz
            wget -q https://download.redis.io/releases/"$redis_pkg" || log_error "下载 $redis_pkg 失败"
        fi
    fi

    [ ! -f "$redis_pkg" ] && log_error "当前目录无 $redis_pkg"

    tar -zxf "$redis_pkg" -C "$REDIS_BASE" || log_info "解压失败"
    cd "$REDIS_BASE"/redis-"$REDIS_VERSION" || exit
    make || log_error "make 失败"
    make install || log_error "make install 失败"
    log_info "版本打印如下"
    /usr/local/bin/redis-server --version
}

### 设置配置文件redis.conf
generate_redis_conf() {
    log_info "添加配置文件：$REDIS_CONF_DIR/redis.conf"
    cat > "$REDIS_CONF_DIR"/redis.conf << EOF
bind $REDIS_IP
port $REDIS_CLIENT_PORT
tcp-backlog 4096
timeout 600
daemonize yes
pidfile $REDIS_LOG_DIR/redis.pid
logfile "$REDIS_LOG_DIR/redis.log"
dir $REDIS_DATA_DIR
maxclients $REDIS_MAXCLIENTS
maxmemory $REDIS_MAX_MEMORY
appendonly $APPENDONLY
cluster-enabled yes
cluster-config-file nodes-$REDIS_CLIENT_PORT.conf
cluster-node-timeout 15000
masterauth "$REDIS_PASSWORD"
requirepass "$REDIS_PASSWORD"
EOF
    local arch
    arch="$(uname -m)"
    if [ "$arch" = "aarch64" ] || [ "$arch" = "arm64" ]; then
        echo 'ignore-warnings ARM64-COW-BUG' >> "$REDIS_CONF_DIR"/redis.conf
    fi
    # 将配置文件所有者调整为运行用户，便于在线调整参数
    chown "$REDIS_USER":"$REDIS_USER" "$REDIS_CONF_DIR"/redis.conf
}

### 添加systemctl服务 (当配置文件中daemonize的值为yes时，Type的值为forking；daemonize的值为no时，Type的值为simple)
generate_redis_service() {
    log_info "添加服务: redis.service"
    cat > /usr/lib/systemd/system/redis.service << EOF
[Unit]
Description=Redis Server
Documentation=https://redis.io/docs/
After=network.target

[Service]
Type=forking

User=$REDIS_USER
Group=$REDIS_USER

ExecStart=/usr/local/bin/redis-server $REDIS_CONF_DIR/redis.conf
ExecStop=/usr/local/bin/redis-cli -p $REDIS_CLIENT_PORT shutdown

PrivateTmp=true

LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
    log_info "启动服务"
    systemctl daemon-reload
    systemctl enable --now redis
    systemctl status redis
}

### 检查Redis服务运行状态
chekc_redis_status() {
    export REDISCLI_AUTH="$REDIS_PASSWORD"
    while :
    do  
        local ok_nodes=()
        log_info "检查Redis服务运行状态，循环间隔15s"
        for var in "${REDIS_CLUSTER_IP[@]}"
        do
            if /usr/local/bin/redis-cli -h "$var" -p "$REDIS_CLIENT_PORT" ping | grep -q "PONG"; then
                log_success "节点 $var:$REDIS_CLIENT_PORT 运行正常"
                ok_nodes+=("$var:$REDIS_CLIENT_PORT")
            else
                log_warn "节点 $var:$REDIS_CLIENT_PORT 运行异常"
            fi
        done
        if [ ${#ok_nodes[@]} -eq ${#REDIS_CLUSTER_IP[@]} ]; then
            log_success "Redis集群运行正常, 将关联集群！"
            break
        else
            log_warn "部分 Redis 节点运行异常，当前正常运行节点: ${ok_nodes[*]}，进入下一轮循环"
        fi
        sleep 15s
    done
}

### 关联集群
create_redis_cluster() {
    local cluster_nodes=()
    if [ "${REDIS_CLUSTER_IP[0]}" = "$REDIS_IP" ]; then
        for ip in "${REDIS_CLUSTER_IP[@]}"; do
            cluster_nodes+=("${ip}:${REDIS_CLIENT_PORT}")
        done
        log_warn "将在节点 ${REDIS_CLUSTER_IP[0]} 关联Redis集群，请根据提示信息操作"
        export REDISCLI_AUTH="$REDIS_PASSWORD"
        /usr/local/bin/redis-cli --cluster create "${cluster_nodes[@]}" --cluster-replicas 1 --cluster-yes
    else
        log_info "将在节点 ${REDIS_CLUSTER_IP[0]} 关联Redis集群"
    fi
}

main() {
    log_info "开始安装Redis集群 (版本: ${REDIS_VERSION})"
    check_dependency
    check_ports
    init_env
    if [ -f /usr/local/bin/redis-cli ] \
        && [ -f /usr/local/bin/redis-server ] \
        && [ -f /usr/local/bin/redis-sentinel ]; then
        log_warn "Redis 已安装 \n $(/usr/local/bin/redis-server --version)"
        while :
        do
            read -p "Redis 已安装，是否重新编译安装？(Y/n) " -r option
            case "${option}" in
                Y|y)
                    log_info "用户选择重新安装"
                    install_make_redis
                ;;
                N|n)
                    log_info "用户跳过重新安装"
                    break
                ;;
                *)
                    log_warn "输入不合法，请输入Y或N"
                ;;
            esac
        done
    else
        install_make_redis
    fi
    generate_redis_conf
    generate_redis_service
    chekc_redis_status
    create_redis_cluster
    log_success "Redis集群部署完成!"
}

main